{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 协同过滤推荐"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.模拟数据集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "users = {\"小明\": {\"中国合伙人\": 5.0, \"太平轮\": 3.0, \"荒野猎人\": 4.5, \"老炮儿\": 5.0, \"我的少女时代\": 3.0, \"肖洛特烦恼\": 4.5, \"火星救援\": 5.0},\n",
    "         \"小红\":{\"小时代4\": 4.0, \"荒野猎人\": 3.0, \"我的少女时代\": 5.0, \"肖洛特烦恼\": 5.0, \"火星救援\": 3.0, \"后会无期\": 3.0},\n",
    "         \"小阳\": {\"小时代4\": 2.0, \"中国合伙人\": 5.0, \"我的少女时代\": 3.0, \"老炮儿\": 5.0, \"肖洛特烦恼\": 4.5, \"速度与激情7\": 5.0},\n",
    "         \"小四\": {\"小时代4\": 5.0, \"中国合伙人\": 3.0, \"我的少女时代\": 4.0, \"匆匆那年\": 4.0, \"速度与激情7\": 3.5, \"火星救援\": 3.5, \"后会无期\": 4.5},\n",
    "         \"六爷\": {\"小时代4\": 2.0, \"中国合伙人\": 4.0, \"荒野猎人\": 4.5, \"老炮儿\": 5.0, \"我的少女时代\": 2.0},\n",
    "         \"小李\":  {\"荒野猎人\": 5.0, \"盗梦空间\": 5.0, \"我的少女时代\": 3.0, \"速度与激情7\": 5.0, \"蚁人\": 4.5, \"老炮儿\": 4.0, \"后会无期\": 3.5},\n",
    "         \"隔壁老王\": {\"荒野猎人\": 5.0, \"中国合伙人\": 4.0, \"我的少女时代\": 1.0, \"Phoenix\": 5.0, \"甄嬛传\": 4.0, \"The Strokes\": 5.0},\n",
    "         \"邻村小芳\": {\"小时代4\": 4.0, \"我的少女时代\": 4.5, \"匆匆那年\": 4.5, \"甄嬛传\": 2.5, \"The Strokes\": 3.0}\n",
    "        }"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.定义距离计算函数\n",
    "- 自定义实现距离计算函数集，也可以使用scipy库中的内置计算函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from math import sqrt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.1 欧式距离\n",
    "- 直接求两个向量之间的距离，计算2个打分序列间的欧式距离. 输入的rating1和rating2都是打分dict，格式为{'小时代4': 1.0, '疯狂动物城': 5.0, ...}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "def euclidean_distance(rating1, rating2):\n",
    "    distance = 0\n",
    "    # 是否存在共同的电影\n",
    "    commonRating = False\n",
    "    for key in rating1:\n",
    "        if key in rating2:\n",
    "            distance += (rating1[key] - rating2[key]) ** 2\n",
    "            commonRating = True\n",
    "    if commonRating:\n",
    "        return distance\n",
    "    # 无公共打分电影\n",
    "    return -1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.2 曼哈顿距离：一范式\n",
    "- 计算2个打分序列间的曼哈顿距离. 输入的rating1和rating2都是打分dict，格式为{'小时代4': 1.0, '疯狂动物城': 5.0}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def manhattan_dis(rating1, rating2):\n",
    "    distance = 0\n",
    "    # 是否存在共同的电影\n",
    "    commonRating = False\n",
    "    for key in rating1:\n",
    "        if key in rating2:\n",
    "            distance += abs(rating1[key] - rating2[key])\n",
    "            commonRating = True\n",
    "    if commonRating:\n",
    "        return distance\n",
    "    # 无公共打分电影\n",
    "    return -1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.3 余弦相似度\n",
    "- 计算2个打分序列间的cos距离. 输入的rating1和rating2都是打分dict，格式为{'小时代4': 1.0, '疯狂动物城': 5.0}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def cos_dis(rating1, rating2):\n",
    "    distance = 0\n",
    "    dot_product_1 = 0\n",
    "    dot_product_2 = 0\n",
    "    commonRatings = False\n",
    "    \n",
    "    for key in rating1:\n",
    "        if key in rating2:\n",
    "            distance += rating1[key] * rating2[key]\n",
    "            commonRatings = True\n",
    "    \n",
    "    for score in rating1.values():\n",
    "        dot_product_1 += score ** 2\n",
    "    for score in rating2.values():\n",
    "        dot_product_2 += score ** 2\n",
    "        \n",
    "    # 两个序列见存在公共电影\n",
    "    if commonRatings:\n",
    "        return 1 - distance / sqrt(dot_product_1 * dot_product_2)\n",
    "    return -1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.4 皮尔森相关系数\n",
    "- 计算2个打分序列间的pearson距离. 输入的rating1和rating2都是打分dict，格式为{'小时代4': 1.0, '疯狂动物城': 5.0, ...}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "def pearson_dis(rating1, rating2):\n",
    "    sum_xy = 0\n",
    "    sum_x = 0\n",
    "    sum_y = 0\n",
    "    sum_x2 = 0\n",
    "    sum_y2 = 0\n",
    "    n = 0\n",
    "    for key in rating1:\n",
    "        if key in rating2:\n",
    "            n += 1\n",
    "            x = rating1[key]\n",
    "            y = rating2[key]\n",
    "            sum_xy += x * y\n",
    "            sum_x += x\n",
    "            sum_y += y\n",
    "            sum_x2 += pow(x ,2)\n",
    "            sum_y2 += pow(y, 2)\n",
    "    denominator = sqrt(sum_x2 - pow(sum_x, 2) / n) * sqrt(sum_y2 - pow(sum_y, 2) / n)\n",
    "    if denominator == 0:\n",
    "        return 0\n",
    "    return (sum_xy - (sum_x * sum_y) / n) / denominator"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. 查找最近邻\n",
    "- 基于用户的协同过滤算法查找相似度高的物品/用户序列。计算用户username和其他用户序列的相似度，并进行TopN排序"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "def computeNearestNeighbor(username, users):\n",
    "    distances = []\n",
    "    for user in users:\n",
    "        if user != username:\n",
    "            distance = pearson_dis(users[user], users[username])\n",
    "            distances.append((distance, user))\n",
    "    # 根据距离排序，距离越近，排序越靠前\n",
    "    distances.sort()\n",
    "    return distances"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.电影推荐\n",
    "- 在用户相似度很高的电影topN排序中，推荐给username没有看过的电影"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "def recommend(username, users):\n",
    "    # 查找最近邻\n",
    "    nearest = computeNearestNeighbor(username, users)[0][1]\n",
    "    # 推荐的电影序列\n",
    "    recommendations = []\n",
    "    # 获取最近邻用户看的电影及评分\n",
    "    neighborRatings = users[nearest]\n",
    "    print(\"neighborRatings =>\", neighborRatings)\n",
    "    # 获取当前用户看的电影及评分\n",
    "    currentRatings = users[username]\n",
    "    for movie in neighborRatings:\n",
    "        if not movie in currentRatings:\n",
    "            recommendations.append((movie, neighborRatings[movie]))\n",
    "    print(\"recommendations =>\", recommendations)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "neighborRatings => {'小时代4': 4.0, '荒野猎人': 3.0, '我的少女时代': 5.0, '肖洛特烦恼': 5.0, '火星救援': 3.0, '后会无期': 3.0}\n",
      "recommendations => [('肖洛特烦恼', 5.0), ('火星救援', 3.0), ('后会无期', 3.0)]\n"
     ]
    }
   ],
   "source": [
    "recommend('六爷', users)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
